FalkorDB backend (server + embedded Lite), replacing NetworkX - #1379
FalkorDB backend (server + embedded Lite), replacing NetworkX#1379galshubeli wants to merge 20 commits into
Conversation
Replace NetworkX with FalkorDB as the sole graph store, via a cache-free GraphStore facade: the graph lives in the engine and is never loaded into Python. All traversal/query/analysis run as scoped or in-engine Cypher; algorithms FalkorDB lacks (Louvain, edge-betweenness, simple-cycles) are server-side JS UDFs (graphify/udfs/graphify_algos.js). multigraph_compat is removed (native parallel edges). Query-path optimizations: - truncate-before-fetch rendering: rank by degree + LIMIT in-engine, fetch attrs only for the nodes that fit the token budget (hub query 7-14s -> ~1s) - in-engine edge-context filtering (no MemGraph copy of the whole graph) - hub-degree threshold computed once and cached in graph metadata Audit fixes: MCP tools get_node / graph_stats / audit no longer scan all nodes/edges (use search_nodes + a confidence_counts aggregation). NetworkX fully removed (not a dependency): the package has zero runtime nx imports; the test suite builds fixtures via tests/nxcompat.py, a drop-in nx shim backed by the in-house MemGraph. FalkorDB Lite: embedded in-process engine (redislite + bundled falkordb module), no external server. Select with a `falkordb-lite://<rdb>` URI or the GRAPHIFY_FALKORDB_LITE env var; install via the new `lite` extra (Python >=3.12). Warm performance matches the server (no TCP hop) at comparable memory. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t load Post-migration these open a cache-free FalkorDB store handle (a connection) and never load the graph into memory, so the NetworkX-era "load" naming was misleading: - serve._load_graph -> serve._connect_graph - affected.load_graph -> affected.connect_graph - CLI messages: "Loading existing graph..." -> "Connecting to existing graph..."; "could not load graph" -> "could not connect to graph" - clarified the MCP hot-reload docstring (it reconnects when the graph.json change-sentinel is rewritten; it does not reload the file) Genuine loaders are unchanged: callflow_html.load_graph (parses graph.json into data), the merge-driver graph.json loaders, and _load_graphifyignore/include. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From section-by-section review of the migration: - __main__.py: `affected` and `cluster-only` no longer require the legacy graph.json file to exist on disk. In FalkorDB-only mode the graph lives in the engine (located via the falkordb.json pointer) and the artifact may be absent; these commands now rely on _connect_graph's pointer resolution + empty-graph guard, matching `query`/`path`/`explain`. Previously `query` worked but `affected`/`cluster-only` failed with "graph file not found". - store.py: UDFs (louvain/edgeBetweenness/simpleCycles) are now loaded lazily by the algorithm wrappers that use them instead of in GraphStore.__init__, so the interactive commands (query/path/explain/affected) — which never use UDFs — no longer pay a UDF reload round-trip on every process. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From continued section-by-section review: - analyze.suggest_questions: cap exact node-betweenness at 5000 nodes (same guard _cross_community_surprises already uses). Exact betweenness is ~O(V*E); without a cap, report/cluster-only could be very slow on large repos. The bridge-node questions are supplementary — ambiguous-edge + community questions are unaffected. - export.to_svg: render a top-degree overview (>600 nodes) instead of the full graph. The pure-Python force layout (_spring_layout) is O(n^2) per iteration, so a full-graph SVG of a large graph would effectively hang (verified: a 15k-node graph now renders a 600-node overview in ~9s instead of never). An SVG of thousands of nodes is an unreadable hairball anyway. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the gaps found reviewing PR #1: - affected.connect_graph now guards the empty-graph case (raises so the CLI prints "Re-run /graphify to build" + exits non-zero) instead of handing back an empty store and silently reporting "no unique node match". Parity with serve._connect_graph and the query/path/explain commands. - tests: validate the hand-rolled GraphML writer emits well-formed XML (xml.etree parse), add a to_svg export test (skipped without matplotlib), and add an affected unbuilt-graph test that asserts a non-zero exit (covers the guard above). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci.yml: run falkordb/falkordb as a service container so the conftest session fixture connects (otherwise every graph test silently skips in CI). - cluster.py: materialize dict(G.degree()) once instead of per-node G.degree(n) calls, which against the cache-free store would be O(N) round-trips (twice). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve 39 conflicts, reconciling the FalkorDB migration (cache-free GraphStore, no networkx) with v8's improvements: - pyproject: keep falkordb + numpy (v8's _minhash replaced datasketch); drop networkx + datasketch. - build.py: keep v8's dedupe_nodes/dedupe_edges helpers + Graphify-Labs#1257 ghost-collision tracking + Graphify-Labs#1327 AST-origin skip, adapted to node_attrs (no in-memory G during build); keep the GraphStore build_from_json. - serve.py: adopt v8 label_tokens matching (Graphify-Labs#1353) + community_name (Graphify-Labs#1305) on the cache-free _find_node/_tool_get_node paths. - analyze.py: keep the simpleCycles UDF path (max_cycle_length bounds it like v8's length_bound Graphify-Labs#1196). - export.py: keep store-native to_json; fold in v8 community_name (Graphify-Labs#1305). - global_graph.py: keep the GraphStore batch path; adopt v8 remap-and-rewire of deduplicated externals. - affected.py: keep FalkorDB connect_graph + find_node_ids; adopt v8 normalized + bare-name matching (Graphify-Labs#1353) in the in-memory fallback. - __main__.py: keep _connect_graph loading; preserve v8's Graphify-Labs#1019 huge-graph html guard by node count; adopt community_name in explain. - skill docs regenerated from the merged update fragment (prune = deleted + changed, Graphify-Labs#1178); update goldens blessed. - ci.yml: FalkorDB service container. - tests resolved to the migration versions (seed_graph/store/nxcompat); v8's graph.json-loading tests no longer apply. Verified: package imports, zero runtime networkx, 279/280 merge-relevant tests pass (1 env tree-sitter fail). Remaining full-suite failures are pre-existing env (missing tree-sitter parsers) — test_watch prune-edge-case fails at the pre-merge commit too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pyproject: add `falkordb` extra so the documented `graphifyy[falkordb]` install resolves (falkordb is a core dep; the extra just re-lists it). - conftest: the FalkorDB reachability skip is no longer session-autouse — the store/seed_graph/make_store fixtures depend on it, so only DB-backed tests skip when no engine is reachable (DB-free tests still run; CI can't silently pass with 0 tests). - update skill doc: the graph-diff step used a global `graphify_diff_scratch` graph name (data-loss if a user has that graph; collisions on concurrent runs). Derive a per-project + pid scratch name from the current graph name. Regenerated skill artifacts + blessed goldens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v8 merge folded label_tokens matching (Graphify-Labs#1353) into _find_node, but the cache-free rows are (id, norm_label) tuples with no `d` in scope — a NameError on every _find_node call (explain / get_node / score). Carry the raw label in rows (3-tuple) and tokenize that, mirroring _score_nodes. This fix existed in the working tree but was not staged into the merge commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1116) A symbol removed from a file that still exists was not pruned on `graphify update` / watch rebuild: the eviction keys off the AST-provenance marker `_origin` in graph.json, but the FalkorDB migration stripped all underscore-prefixed keys at the store boundary (_scalar_props) and in to_json, so `_origin` never reached graph.json and stale AST nodes (e.g. a deleted `foo()`) survived. (Pre-existing since the migration; surfaced once tree-sitter parsers are installed.) - store.add_nodes_from: keep `_origin` (the one underscore-marker that must persist); other `_` keys stay dropped. - export.to_json: keep `_origin` on exported nodes. - watch._canonical_topology_for_compare: exclude `_origin` (provenance, not topology) so an unchanged graph still compares equal across serializers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lockfile predated the migration's pyproject changes, so it had no falkordb
entry (and still pinned networkx). CI installs with `uv sync --all-extras
--frozen`, which used that stale lock and left the falkordb client uninstalled
-> every DB test ImportError'd ("No module named 'falkordb'").
Re-resolved against the current pyproject: adds falkordb 1.6.1, falkordblite
(py>=3.12 marker), redis, psutil; bumps graphifyy to 0.8.40. The FalkorDB
service container (added to ci.yml) provides the server the tests connect to.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two CI failures (surfaced once the FalkorDB client installs and tree-sitter parsers are present in CI): - test_java_type_resolution used G.in_degree(v); GraphStore only had degree. Add in_degree/out_degree (scoped counts) to GraphStore and MemGraph. - test_build_merge_preserves_call_edge_direction called build_merge([], graph_path) — the migration's build_merge takes a graph_name, not a graph.json path, so the Path reached select_graph() and raised "Expected a string parameter". Reconnect via G1.graph_name instead. (Was masked locally because the JS parser isn't installed.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflicts after upstream advanced (Java records, Swift relationships, build_merge replace-on-re-extract Graphify-Labs#1344, dedup fixes, HTML XSS hardening, etc.): - build.py build_merge: adopt v8's replace-on-re-extract (Graphify-Labs#1344/Graphify-Labs#1007) — drop a changed file's prior source_file contribution before merging so stale nodes/edges don't accumulate — but keep loading the base from the FalkorDB store (not graph.json), and keep graph_name/uri binding. - skillgen update fragment: keep the FalkorDB _store/open_store binding for the --update build_merge call; fold in v8's root= prune-relativization note (Graphify-Labs#1361). Regenerated skill artifacts + blessed goldens. - test_build.py: keep the store-based tests; v8's new graph.json/build_merge(path) tests don't apply to the store-based build_merge. Verified: package imports, zero runtime networkx, 118 merge-relevant tests pass (1 env-only tree-sitter fail), skillgen --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sort each match tier (exact/prefix/substring) by node id so an ambiguous label resolves to the same top match regardless of the backend's row order (FalkorDB scan order vs. in-memory iteration). Found via native-vs-reference parity review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aging, edge direction, seed resolution) Four confirmed regressions found in external review of the FalkorDB backend: - build.py: edges missing source_file crashed build_from_json with NameError (referenced an undefined `G`); backfill from node_attrs instead. - pyproject.toml: package the FalkorDB UDFs (udfs/*.js) so wheel/sdist installs don't FileNotFoundError on Louvain/edge-betweenness/cycles. - store.py: directed edge semantics — remove_edges deletes only src->tgt (was nuking reciprocal edges); G[u][v] / _edge_attrs prefer the forward edge while still finding a reverse edge (undirected-model lookup). - affected.py: native resolve_seed gained the bare-callable-name tier (`foo` -> `foo()`) it had only on the in-memory fallback path. Adds regression tests for each. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_traverse marked nodes visited mid-level, so a node reached from several parents in the same BFS level kept only the first parent's edge — thinning ~1/3 of edges in dense subgraphs that `query` (and MCP) return to the user. Defer the visited update to end-of-level (mirroring v8 _bfs) and RETURN DISTINCT to dedup parallel edges. Edge sets now match the in-memory reference exactly (verified on the 15k-node graph: 1964/1964 on the densest seed). Found via PR Graphify-Labs#1379 comprehensive review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… graph.json) A read command (query/path/explain/export/cluster-only) opening an empty FalkorDB store now imports a node-link graph.json from the output dir on first use, instead of failing with "graph empty". This preserves the prior workflow for projects built before the FalkorDB backend and for `--no-cluster` runs (which write only JSON) — existing graphs stay queryable without a rebuild. One-time, transparent (prints a migration note); no-op when no graph.json exists. Found via PR Graphify-Labs#1379 comprehensive review (Graphify-Labs#3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When no FalkorDB server is reachable at the local default, _connect now transparently starts the embedded engine (FalkorDB Lite) instead of failing, so a plain install runs with no server to manage — matching the pre-FalkorDB zero-setup experience. falkordblite is now a default dependency on Python >= 3.12; on older interpreters a running server is still required (clear error). A connection failure to an explicitly-configured REMOTE host still surfaces as an error rather than silently using a local empty DB. Shared embedded RDB lives under XDG_DATA_HOME/graphify (override: GRAPHIFY_LITE_DBFILE). Found via PR Graphify-Labs#1379 comprehensive review (Graphify-Labs#2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v8 split the monolithic __main__.py into a thin shim + graphify/cli.py. The FalkorDB backend was written against the monolith, so cli.py merged in clean but entirely un-ported: 84 NetworkX/graph.json references and zero open_store calls. Since this branch drops networkx as a dependency, that would have merged green and ImportError'd on a default install. Resolution: - __main__.py: take v8's shim; replay our 19 hunks onto cli.py's dispatch_command (8 applied, 11 hand-ported). - serve/build/store/affected/export/benchmark: combine both sides, keeping v8's fixes (Graphify-Labs#2074 honest relations, #BUG1 call sites, #BUG2 seed-first truncation, Graphify-Labs#1504 re-key, Graphify-Labs#1749 cross-language guard, Graphify-Labs#1831 GraphML coercion, Graphify-Labs#2032 label disambiguation) on the store-backed paths. - 70 generated skill artifacts regenerated from the two conflicted fragments; `skillgen --check` passes on all 134. - uv.lock regenerated from the merged pyproject. Store extended so upstream fixes survive the backend swap: - edge_attrs_all(): _edge_attrs used LIMIT 1, collapsing parallel edges and defeating Graphify-Labs#2074's honest relation reporting. - node_connections()/subgraph_render_data(): return the edge's own source_file/source_location and true orientation via startNode(r), so #BUG1 call sites and edge direction survive. - incoming_edges()/member_nodes(): give affected's native path the #BUG1 locations and Graphify-Labs#1669 member seeding the in-memory path already had. Bugs found and fixed while porting: - open_store derived the graph name from the PARENT of the output dir, so two graph dirs sharing a parent silently shared one FalkorDB graph. - A relation-less edge was stored with relation="RELATED_TO", so readers could not distinguish "no relation" from a real one. - god-nodes imported the removed affected.load_graph; affected/god-nodes also could not read pre-FalkorDB graphs that query/explain could. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review panel
Reviewed by claude, gpt.
This PR migrates the affected and analyze graph operations from an in-memory NetworkX representation to a FalkorDB-backed store, adding native scoped/batched query paths (e.g. find_node_ids, incoming_edges, node_attrs_batch) while keeping in-memory fallbacks. It replaces load_graph with a connect_graph helper that opens the store and imports legacy graph.json on first use, and refactors label/file-node helpers to operate on attribute rows rather than graph objects. It also adds a FalkorDB service container to the CI workflow so graph-dependent tests run against a live backend instead of being skipped. Surface area also includes numerous updated test and skillgen expectation fixtures referenced in the changed-symbols list.
No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
graphify review
Impact — 2485 function(s) in the blast radius of 2070 changed node(s).
Health — regressions introduced by this change:
- worsened (Ca·Ce 1794→2502):
build_from_json()(Ca=139 Ce=18) — fans out to 18 callees (efferent coupling); 139 callers depend on it (afferent coupling) - worsened (Ca·Ce 2400→2450):
_rebuild_code()(Ca=50 Ce=49) — fans out to 49 callees (efferent coupling); 50 callers depend on it (afferent coupling) - worsened (Ca·Ce 218→220):
dispatch_command()(Ca=2 Ce=110) — fans out to 110 callees (efferent coupling) - worsened (Ca·Ce 144→175):
_make_graph()(Ca=25 Ce=7) — fans out to 7 callees (efferent coupling); 25 callers depend on it (afferent coupling) - worsened (Ca·Ce 112→128):
to_html()(Ca=16 Ce=8) — fans out to 8 callees (efferent coupling); 16 callers depend on it (afferent coupling) - new offender:
open_store()(Ca=25 Ce=4) — 25 callers depend on it (afferent coupling) - new offender:
Graph(Ca=44 Ce=2) — 44 callers depend on it (afferent coupling) - new offender:
_connect_graph()(Ca=12 Ce=7) — fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling) - worsened (Ca·Ce 39→56):
_find_node()(Ca=14 Ce=4) — 14 callers depend on it (afferent coupling) - new offender:
DiGraph(Ca=21 Ce=2) — 21 callers depend on it (afferent coupling) - worsened (Ca·Ce 30→33):
resolve_seed()(Ca=11 Ce=3) — 11 callers depend on it (afferent coupling) - worsened (Ca·Ce 18→27):
to_graphml()(Ca=9 Ce=3) — 9 callers depend on it (afferent coupling) - new offender:
_connect()(Ca=7 Ce=3) — 7 callers depend on it (afferent coupling) - worsened (Ca·Ce 0→21):
_make_graph()(Ca=21 Ce=1) — 21 callers depend on it (afferent coupling) - worsened (Ca·Ce 0→16):
_graph_file()(Ca=16 Ce=1) — 16 callers depend on it (afferent coupling) - new offender:
_import_graph_json_into_store()(Ca=7 Ce=2) — 7 callers depend on it (afferent coupling)
Verification — 2485 function(s) need re-proving (callee-first):
tools_skillgen_fragments_references_shared_update_graphify_reference_incremental_update_and_cluster_only → tools_skillgen_fragments_references_shared_update_for_update_incremental_re_extraction → tools_skillgen_fragments_references_shared_update_for_cluster_only → tools_skillgen_fragments_references_shared_update → tools_skillgen_fragments_references_query_default_step_1_traversal → tools_skillgen_fragments_references_query_default_step_0_constrained_query_expansion_required_before_traversal → tools_skillgen_fragments_references_query_default_graphify_reference_query_path_explain → tools_skillgen_fragments_references_query_default_for_graphify_path → tools_skillgen_fragments_references_query_default_for_graphify_explain → tools_skillgen_fragments_references_query_default …
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not enforced this run (re-run with --prove to verify the blast radius)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2468 function(s) in blast radius — re-run with
--proveto formally verify them
· 9 grounded finding(s) anchored inline below; 7 more finding(s) on lines outside this diff (see the check run).
|
|
||
|
|
||
| def resolve_seed(graph: nx.Graph, query: str) -> str | None: | ||
| def resolve_seed(graph, query: str) -> str | None: |
There was a problem hiding this comment.
resolve_seed()
11 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
| directed=True produces a DiGraph that preserves edge direction (source→target). | ||
| directed=False (default) produces an undirected Graph for backward compatibility. | ||
| def build_from_json( |
There was a problem hiding this comment.
build_from_json()
fans out to 18 callees (efferent coupling); 139 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
|
|
||
| def _load_graph(graph_path: str) -> nx.Graph: | ||
| def _connect_graph(graph_path: str): |
There was a problem hiding this comment.
_connect_graph()
fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return sample | ||
|
|
||
|
|
||
| def _import_graph_json_into_store(gj_path: Path, store) -> bool: |
There was a problem hiding this comment.
_import_graph_json_into_store()
7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return Path(out_dir) / "falkordb.json" | ||
|
|
||
|
|
||
| def open_store( |
There was a problem hiding this comment.
open_store()
25 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return redislite.FalkorDB(dbfilename=dbfile) | ||
|
|
||
|
|
||
| def _connect(uri: str, user: str | None = None, password: str | None = None): |
There was a problem hiding this comment.
_connect()
7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return self._build()[key] | ||
|
|
||
|
|
||
| class Graph(_MutableGraph): |
There was a problem hiding this comment.
Graph
44 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| super().__init__(directed=False) | ||
|
|
||
|
|
||
| class DiGraph(_MutableGraph): |
There was a problem hiding this comment.
DiGraph
21 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -47,6 +47,19 @@ | |||
|
|
|||
|
|
|||
| def _graph_file(tmp_path: Path) -> str: | |||
There was a problem hiding this comment.
_graph_file()
16 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Clears the 20 failures CI reported on the merge commit. Behavioral fixes (not test-only): - build_from_json now records the scan root on the graph (`scan_root` meta) and build_merge reads it back when the caller omits root. This is the FalkorDB replacement for the `graphify-out/.graphify_root` marker; without it the skill's root-less --update never relativized absolute prune_sources and a deleted file's nodes survived as ghosts (Graphify-Labs#1571). - to_graphml dropped graph-level attributes entirely, losing the hyperedge layer from every export; they are now emitted as for="graph" keys with the same Graphify-Labs#1831 scalar coercion as nodes/edges. - find_import_cycles required the graph object to expose .simple_cycles(), so it crashed on a plain NetworkX graph passed in by a library caller (networkx exposes simple_cycles as a module function, not a method). The enumeration is a pure function of the edge list, so it moved to store.simple_cycles_from_edges with the FalkorDB UDF kept as the fast path. - merge-graphs emitted directed: true; the combined cross-repo view is undirected, as the old nx.compose path produced (Graphify-Labs#1606). merge_node_link now takes directed=, defaulting to the first input's flag so the same-graph merge-driver still round-trips. - benchmark could not read a pre-FalkorDB / --no-cluster graph.json; it now falls back to the same back-compat import serve and affected use. - The MCP multi-project loader reported "could not connect to graph"; it is a not-found condition and now says so. Test ports (v8 tests written against file-based loading): - test_build_merge_hyperedges_and_prune: seed the store, pass graph_name; the two Graphify-Labs#1571 cases now pin the recorded scan_root instead of the marker file. - test_extract: read edge direction from (u, v) — the _src/_tgt markers the undirected NetworkX storage needed no longer exist. - test_export: build the dict/list-attribute graph as a MemGraph, the only shape that can carry non-scalar attrs (the engine stores scalars only). - test_extract_cli: inject the committed semantic layer into the store, which is the incremental baseline now, not graph.json. - test_src_layout_import_resolution: give each build its own store — without one, both builds target the default graph name and the second silently overwrites the first, comparing a graph with itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This pull request migrates several graph operations (notably affected.py and analyze.py) away from an in-memory NetworkX representation toward a FalkorDB-backed store, adding native code paths that use scoped/batched queries while retaining in-memory fallbacks. It also replaces the JSON graph loader (load_graph) with a connect_graph function that opens the store and imports legacy graph.json data on demand. Additionally, the CI workflow gains a FalkorDB service container so graph-dependent tests actually run, and the touched test files and skill/reference fixtures are updated accordingly.
Worth a look
- connect_graph misidentifies extension-less directory paths —
graphify/affected.py· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2870 functions depend on the 2542 node(s) this change touches.
Health — this change adds coupling hotspots:
- worse:
build_from_json()— 141 callers, 18 callees - worse:
_rebuild_code()— 50 callers, 49 callees - worse:
dispatch_command()— 2 callers, 110 callees - worse:
_make_graph()— 25 callers, 7 callees - worse:
to_html()— 16 callers, 8 callees - new:
open_store()— 27 callers, 4 callees - new:
Graph— 44 callers, 2 callees - new:
_connect_graph()— 12 callers, 7 callees - …and 10 more
Verification — 2870 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2853 function(s) in the blast radius were not formally verified this run
· 9 grounded finding(s) anchored inline below; 9 more finding(s) on lines outside this diff (see the check run).
|
|
||
|
|
||
| def resolve_seed(graph: nx.Graph, query: str) -> str | None: | ||
| def resolve_seed(graph, query: str) -> str | None: |
There was a problem hiding this comment.
resolve_seed()
11 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
| directed=True produces a DiGraph that preserves edge direction (source→target). | ||
| directed=False (default) produces an undirected Graph for backward compatibility. | ||
| def build_from_json( |
There was a problem hiding this comment.
build_from_json()
fans out to 18 callees (efferent coupling); 141 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
|
||
|
|
||
| def _load_graph(graph_path: str) -> nx.Graph: | ||
| def _connect_graph(graph_path: str): |
There was a problem hiding this comment.
_connect_graph()
fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return sample | ||
|
|
||
|
|
||
| def _import_graph_json_into_store(gj_path: Path, store) -> bool: |
There was a problem hiding this comment.
_import_graph_json_into_store()
9 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return Path(out_dir) / "falkordb.json" | ||
|
|
||
|
|
||
| def open_store( |
There was a problem hiding this comment.
open_store()
27 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return redislite.FalkorDB(dbfilename=dbfile) | ||
|
|
||
|
|
||
| def _connect(uri: str, user: str | None = None, password: str | None = None): |
There was a problem hiding this comment.
_connect()
7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| return self._build()[key] | ||
|
|
||
|
|
||
| class Graph(_MutableGraph): |
There was a problem hiding this comment.
Graph
44 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| super().__init__(directed=False) | ||
|
|
||
|
|
||
| class DiGraph(_MutableGraph): |
There was a problem hiding this comment.
DiGraph
21 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -47,6 +47,19 @@ | |||
|
|
|||
|
|
|||
| def _graph_file(tmp_path: Path) -> str: | |||
There was a problem hiding this comment.
_graph_file()
16 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Replace NetworkX with FalkorDB (server and embedded Lite)
This migrates graphify's runtime graph backend from in-memory NetworkX to a cache-free FalkorDB
GraphStore: the graph lives in the engine and is never materialized in Python. The existing NetworkX-shaped API (G.nodes,G.edges,G.degree,G[u][v], …) is preserved as a thin facade, so the ~18 call-site modules were minimally touched, but every read is now a scoped/streamed Cypher query.Highlights
GraphStore— nx-compatible views backed by scoped Cypher; nothing is loaded into Python.falkordblite/redislite, no server) selectable via afalkordb-lite://URI orGRAPHIFY_FALKORDB_LITE; newliteextra (Python ≥ 3.12).networkximports; the test suite builds fixtures via aMemGraph-backednxcompatshim.falkordb/falkordbservice container; bothtest (3.10)andtest (3.12)are green.Benchmark
Per repo, mean over the four operations (
query · path · explain · affected), 3 repetitions. Each cell ismean ± run-to-run standard deviation. Bold = better (lower).🤖 Generated with Claude Code